{
    $Id: header,v 1.1 2000/07/13 06:33:45 michael Exp $
    This file is part of the Free Component Library (FCL)
    Copyright (c) 1999-2000 by the Free Pascal development team

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}
{ Win32 implementation of service application }

{$mode objfpc}{$H+}
uses windows,jwawinsvc;

Const
  CM_SERVICE_CONTROL_CODE = WM_USER+1;
  
Resourcestring
  SErrRegisterHandler  = 'Could not register control handler, error code %d';
  SErrNoControlContext = 'Not handling Control message without control context: (%d %d %d).';
  SControlCodeReceived = 'Control message received: (%d %d %d).';

function StartServiceCtrlDispatcher(lpServiceStartTable: LPSERVICE_TABLE_ENTRY): BOOL; stdcall; external 'advapi32.dll' name 'StartServiceCtrlDispatcherA';
function RegisterServiceCtrlHandlerEx(lpServiceName: LPCSTR;lpHandlerProc: LPHANDLER_FUNCTION_EX; lpContext: LPVOID): SERVICE_STATUS_HANDLE; stdcall;external 'advapi32.dll' name 'RegisterServiceCtrlHandlerExA';
function SetServiceStatus(hServiceStatus: SERVICE_STATUS_HANDLE; const lpServiceStatus: SERVICE_STATUS): BOOL; stdcall; external 'advapi32.dll' name 'SetServiceStatus';

{ ---------------------------------------------------------------------
  Win32 entry points
  ---------------------------------------------------------------------}

Function ServiceControlHandlerEntry(dwControl,dwEventType: DWord; lpEventData,lpContext : Pointer) : DWord; StdCall;

begin
  If (Nil<>lpContext) then
    TDaemonController(lpContext).Controller(dwControl,dwEventType,lpEventData)
  else
    Application.Log(etError,Format(SerrNoControlContext,[dwControl,dwEventType,ptrint(lpEventData)]));
end;

Procedure ServiceMainEntry(Argc : DWord; Args : Pchar); stdcall;

begin
  If Assigned(Application) then
    Application.Main(Argc,PPChar(Args));
{$ifdef svcdebug}Debuglog('Main end');{$endif}
end;

{ ---------------------------------------------------------------------
  TDaemonStartThread
  ---------------------------------------------------------------------}
Type
  TDaemonStartThread = Class(TThread)
    FEntryTable : PServiceTableEntry;
    FLoopHandle : THandle;
  Public
    Constructor Create(T : PServiceTableEntry; LoopHandle : THandle);
    Procedure DoTerminate; override;
    Procedure Execute; override;
    Property ReturnValue;
  end;

{ TDaemonStartThread }

constructor TDaemonStartThread.create(T: PServiceTableEntry; LoopHandle : THandle);
begin
  FEntryTable:=T;
  FLoopHandle:=LoopHandle;
  FreeOnTerminate:=False;
  ReturnValue:=0;
  Inherited Create(False,DefaultStackSize*2);
end;

procedure TDaemonStartThread.DoTerminate;
begin
  Inherited DoTerminate;
  If (FLoopHandle<>0) then
    PostMessage(FLoopHandle,WM_QUIT,0,0);
end;

procedure TDaemonStartThread.Execute;
begin
{$ifdef svcdebug}DebugLog('Calling service dispatcher');{$endif svcdebug}
  if StartServiceCtrlDispatcher(FEntryTable) then
    ReturnValue:=0
  else
    ReturnValue:=GetLastError;
{$ifdef svcdebug}DebugLog('Called service dispatcher');{$endif svcdebug}
end;

{ ---------------------------------------------------------------------
  TSCMData : private data of controller.
  ---------------------------------------------------------------------}

Type
  TSCMData = Class(TObject)
    FHandle : SERVICE_STATUS_HANDLE;
    Constructor Create(AHandle : SERVICE_STATUS_HANDLE);
  end;

Constructor TSCMData.Create(AHandle : SERVICE_STATUS_HANDLE);
begin
  FHandle:=AHandle;
end;

{ ---------------------------------------------------------------------
  TSMData : private data of Application.
  ---------------------------------------------------------------------}

Type

  { TSMData }

  TSMData = Class(TObject)
  Private
    FHandle : SC_HANDLE;
  Public
    Constructor Create(AHandle : SC_HANDLE);
    Destructor Destroy; override;
  end;

Constructor TSMData.Create(AHandle : SC_HANDLE);
begin
  FHandle:=AHandle;
end;

destructor TSMData.Destroy;
begin
  inherited;
end;

{ ---------------------------------------------------------------------
  TCustomDaemonApplication
  ---------------------------------------------------------------------}
  
const
  WinServiceTypes : array[TServiceType] of Integer
                  = (SERVICE_WIN32_OWN_PROCESS, SERVICE_KERNEL_DRIVER,
                     SERVICE_FILE_SYSTEM_DRIVER);

  WinStartTypes : array[TStartType] of Integer
                = (SERVICE_BOOT_START, SERVICE_SYSTEM_START,
                   SERVICE_AUTO_START, SERVICE_DEMAND_START,
                   SERVICE_DISABLED);
  WinErrorSeverities : array[TErrorSeverity] of Integer
                     = (SERVICE_ERROR_IGNORE, SERVICE_ERROR_NORMAL,
                        SERVICE_ERROR_SEVERE, SERVICE_ERROR_CRITICAL);

// Careful, result of this function must be freed !!

Function GetDependencies(D : TDependencies) : PChar;
  
var
  I,L : Integer;
  P   : PChar;
begin
  Result:=Nil;
  L:=0;
  for i:=0 to D.Count-1 do
    Inc(L, Length(D[i].Name)+1+Ord(D[i].IsGroup));
  if (L<>0) then
    begin
    Inc(L); // For final null-terminator;
    GetMem(Result,L);
    P:=Result;
    for i:=0 to D.Count - 1 do
      begin
      if D[i].IsGroup then
        begin
        P^:=Char(SC_GROUP_IDENTIFIER);
        Inc(P);
        end;
      P:=StrECopy(P,PChar(D[i].Name));
      Inc(P);
      end;
    P^:=#0;
    end;
end;

Procedure TCustomDaemonApplication.SysInstallDaemon(Daemon : TCustomDaemon);

Var
  SM,SV: SC_HANDLE;
  N,DN,LG,UN,UP : WideString;
  DD : TDaemonDef;
  ST,STT,ES: Integer;
  IDTag : DWord;
  PIDTag : LPDWord;
  PDeps: PChar;
  lpDependencies,PN,PP : PWideChar;
  SvcDesc, Exe:WideString;
  ServiceDesc : TServiceDescriptionW;
  
begin
  SM := TSMData(FSysData).FHandle;
  DD := Daemon.Definition;
  Exe := WideString( Paramstr(0));
  If (Pos(' ',Exe) <> 0) then
    Exe:='"' + Exe + '"';
  Exe := Exe + ' --run'; // Add --run argument;
  If (DD.RunArguments<>'') then
    Exe := Exe + ' ' + WideString( DD.RunArguments);
  N := WideString(DD.Name);
  DN := WideString(DD.DisplayName);
  
  With DD.WinBindings do
    begin
    LG:=WideString(GroupName);
    UN:=WideString(UserName);
    If (UN='') then
      PN:=Nil
    else
      PN:=PWideChar(UN);
    UP:=WideString(Password);
    if (UP='') then
      PP:=Nil
    else
      PP:=PWideChar(UP);
    // ServiceType ST
      ST:=WinServiceTypes[ServiceType];
    if (doInteractive in DD.Options) and (ServiceType=stWin32)  then
      ST:=ST or SERVICE_INTERACTIVE_PROCESS;
    // Actually, we should check count of enabled definitons only :/
    if (ServiceType=stWin32) and (FMapper.DaemonDefs.Count>1) then
      ST:=(ST xor SERVICE_WIN32_OWN_PROCESS) or SERVICE_WIN32_SHARE_PROCESS;
    // StartType STT
    STT:=WinStartTypes[StartType];
    if (StartType in [stBoot, stSystem]) and (ServiceType<>stDevice) then
      STT:=SERVICE_AUTO_START;
    IDTag:=IDTag;
    If (IDTag<>0) then
      PIDTag:=@IDTag
    else
      PIDTag:=Nil;
    ES:=WinErrorSeverities[ErrorSeverity];
    PDeps := GetDependencies(Dependencies);
    lpDependencies := PWideChar(WideString(string(PDeps)));
    end;
  Try
    SV:=CreateServiceW(SM, PWideChar(N), PWideChar(DN), SERVICE_ALL_ACCESS, ST, STT, ES,
                      PWideChar(Exe), PWideChar(LG), PIDTag, lpDependencies, PN, PP);
    If (SV=0) then
      RaiseLastOSError;
    Try
      SvcDesc:=WideString(DD.Description);
      ServiceDesc.lpDescription:=PWidechar(SvcDesc);
      ChangeServiceConfig2W(SV,SERVICE_CONFIG_DESCRIPTION,@ServiceDesc);
      If (PIDTag<>Nil) then
        DD.WinBindings.IDTag:=IDTag;
    finally
      CloseServiceHandle(SV);
    end
  Finally
    If (PDeps<>Nil) then
      FreeMem(PDeps);
  end;
end;

procedure TCustomDaemonApplication.SysUnInstallDaemon(Daemon: TCustomDaemon);

var
  SM,SV : SC_HANDLE;
  DN : String;
begin
  SM:=TSMData(FSysData).FHandle;
  DN:=Daemon.Definition.Name;
  SV:=OpenService(SM,PChar(DN),SERVICE_ALL_ACCESS);
  if (SV=0) then
    RaiseLastOSError;
  try
    if Not DeleteService(SV) then
      RaiseLastOSError;
  finally
    CloseServiceHandle(SV);
  end;
end;

procedure TCustomDaemonApplication.SysStartUnInstallDaemons;
Var
  SM : SC_HANDLE;

begin
  SM:=OpenSCManager(Nil,Nil,SC_MANAGER_ALL_ACCESS);
  if (SM=0) then
    RaiseLastOSError;
  FSysData:=TSMData.Create(SM);
end;

procedure TCustomDaemonApplication.SysEndUnInstallDaemons;
begin
  CloseServiceHandle(TSMData(FSysData).FHandle);
  FreeandNil(FSysData);
end;


procedure TCustomDaemonApplication.SysStartInstallDaemons;

Var
  SM : SC_HANDLE;
  
begin
  SM:=OpenSCManager(Nil,Nil,SC_MANAGER_ALL_ACCESS);
  if (SM=0) then
    RaiseLastOSError;
  FSysData:=TSMData.Create(SM);
end;

procedure TCustomDaemonApplication.SysEndInstallDaemons;
begin
  CloseServiceHandle(TSMData(FSysData).FHandle);
  FreeandNil(FSysData);
end;

procedure TCustomDaemonApplication.SysStartRunDaemons;
begin
  // Do nothing.
end;

Function TCustomDaemonApplication.RunGUIloop(P : Pointer) : integer;

Const
  HandleOpts=WS_POPUP or WS_CAPTION or WS_CLIPSIBLINGS or WS_SYSMENU or WS_MINIMIZEBOX;

Var
  T : TDaemonStartThread;
  Msg : TMsg;
  TClass: TWndClassA;
  AWClass: TWndClassA = (
    style: 0;
    lpfnWndProc: @DefWindowProcA;
    cbClsExtra: 0;
    cbWndExtra: 0;
    hInstance: 0;
    hIcon: 0;
    hCursor: 0;
    hbrBackground: 0;
    lpszMenuName: nil;
    lpszClassName: 'TDaemonApplication');

begin
  If (GUIHandle=0) then
    begin
    if not GetClassInfoA(HInstance,AWClass.lpszClassName,TClass) then
      begin
      AWClass.hInstance := HInstance;
      if Windows.RegisterClassA(AWClass) = 0 then
        DaemonError(SErrWindowClass);
      end;
    GUIHandle := CreateWindowA(AWClass.lpszClassName, Pchar(Title),
      HandleOpts, 1,1, 0, 0, 0, 0, HInstance, nil);
    end;
  T:=TDaemonStartThread.Create(P,GUIHandle);
  Try
    If Assigned(GuiMainLoop) then
       GUIMainLoop
    else
      begin
      // Run a message loop.
      Msg.Message:=0;
      Repeat
        if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
          begin
          if (Msg.Message<>WM_QUIT) and (Msg.Message<>WM_ENDSESSION) then
            begin
            TranslateMessage(Msg);
            DispatchMessage(Msg);
            end
          else
            Terminate;
          end;
        if not Terminated then
          WaitMessage;  
      Until Terminated;
      end;
  finally
    Result:=T.ReturnValue;
    T.Free;
  end;
end;

procedure TCustomDaemonApplication.SysEndRunDaemons;

Var
  P : PServiceTableEntry;
  I,C : Integer;
  RV : Integer;
  
begin
  GetMem(P,SizeOf(TServiceTableEntry)*(FMapper.DaemonDefs.Count+1));
  Try
    C:=FMapper.DaemonDefs.Count;
    For I:=0 to C-1 do
      If Assigned(FMapper.DaemonDefs[i].Instance) then
        begin
        P[i].lpServiceName:=Pchar(FMapper.DaemonDefs[i].Name);
        P[i].lpServiceProc:=@ServiceMainEntry;
        end;
    // Set last entry to Nil.
    P[C].lpServiceName:=Nil;
    P[C].lpServiceProc:=Nil;
    If IsConsole then
      begin
       {$ifdef svcdebug}DebugLog('Starting ctrl dispatcher');{$endif svcdebug}
      Try
        if StartServiceCtrlDispatcher(P) then
          begin
          {$ifdef svcdebug}DebugLog('Return of dispatcher OK');{$endif svcdebug}
          RV:=0;
          end
        else
          begin
          RV:=GetLastError;
          {$ifdef svcdebug}DebugLog('Return of dispatcher NOK');{$endif svcdebug}
          end;
      except
        On E : Exception do
          begin
          {$ifdef svcdebug}Debuglog('Caught exception : '+E.MEssage);{$endif svcdebug}
          Raise;
          end;
      end;
      end
    else
      begin
      RV:=RunGuiLoop(P);
      end;
    {$ifdef svcdebug}DebugLog('SysRun Terminating');{$endif svcdebug}
    Terminate;
    If (RV<>0) then
      Log(etError,Format(SErrServiceManagerStartFailed,[SysErrorMessage(RV)]));
  Finally
    FreeMem(P);
  end;
end;

procedure TCustomDaemonApplication.RemoveController(
  AController: TDaemonController);
  
begin
  FreeAndNil(AController.FDaemon);
  AController.Free;
end;

{ ---------------------------------------------------------------------
  TDaemonThread
  ---------------------------------------------------------------------}


type
  TMessageRec = record
    EventType: DWord;
    EventData: Pointer;
  end;
  PMessageRec = ^TMessageRec;


procedure TDaemonThread.StartServiceExecute;

Var
  Msg : TMsg;
  
begin
  PeekMessage(Msg,0,WM_USER,WM_USER,PM_NOREMOVE);
end;


procedure TDaemonThread.CheckControlMessage(WaitForMessage : Boolean);

Var
  Msg : TMsg;
  StopLoop : Boolean;

begin
  StopLoop:=False;
  Repeat
    StopLoop:=Terminated and WaitForMessage;
    If Not StopLoop then
      begin
      If WaitForMessage Then
        StopLoop:=Not GetMessage(Msg,0,0,0)
      else
        StopLoop:=Not PeekMessage(Msg,0,0,0,PM_REMOVE);
      If Not StopLoop then
        begin
        If (Msg.hwnd<>0) or (Msg.Message<>CM_SERVICE_CONTROL_CODE) then
          DispatchMessage(Msg)
        else if (Msg.Message=CM_SERVICE_CONTROL_CODE) then
        begin
          HandleControlCode(Msg.wParam, PMessageRec(Msg.lParam)^.EventType, PMessageRec(Msg.lParam)^.EventData);
          System.Dispose(PMessageRec(Msg.lParam));
        end
        else
          HandleControlCode(Msg.wParam, 0, Nil);
        end;
      end;
  Until StopLoop;
end;

{ ---------------------------------------------------------------------
  TDaemonController
  ---------------------------------------------------------------------}

procedure TDaemonController.StartService;
begin
  Main(0,Nil);
end;


procedure TDaemonController.Main(Argc: DWord; Args: PPChar);

Var
  T : TThread;
  H : SERVICE_STATUS_HANDLE;
  I : Integer;
  
begin
  For I:=0 to Argc-1 do
    FParams.Add(StrPas(Args[I]));
  H:=RegisterServiceCtrlHandlerEx(Args[0],@ServiceControlHandlerEntry,Self);
  if (H=0) then
    Application.Log(etError,Format(SErrRegisterHandler,[getlasterror]));
  FSysData:=TSCMData.Create(H);
  FDaemon.Status:=csStartPending;
  Try
    T:=TDaemonThread.Create(FDaemon);
    T.Resume;
    T.WaitFor;
    FreeAndNil(T);
    FDaemon.FThread:=Nil;
  except
    On E : Exception do
      FDaemon.Logmessage(Format(SErrDaemonStartFailed,[FDaemon.Definition.Name,E.Message]));
  end;
end;

procedure TDaemonController.Controller(ControlCode, EventType: DWord;
  EventData: Pointer);

Var
  TID : THandle;
  msg: PMessageRec;
begin
  if Assigned(FDaemon.FThread) then
    begin
    TID:=FDaemon.FThread.ThreadID;
    If FDaemon.FThread.Suspended then
      FDaemon.FThread.Resume;
    New(msg);
    msg^.EventType := EventType;
    msg^.EventData := EventData;
    PostThreadMessage(TID,CM_SERVICE_CONTROL_CODE,ControlCode,LPARAM(msg));
    end;
end;


function TDaemonController.ReportStatus: Boolean;

  Function GetAcceptedCodes(ACodes : TWinControlCodes) : Integer;

    function IsWindows7OrNewer: Boolean; inline;
    begin
      Result := (Win32MajorVersion > 6) or
                ((Win32MajorVersion = 6) and (Win32MinorVersion >= 1));
    end;

    function IsWindows8OrNewer: Boolean; inline;
    begin
      Result := (Win32MajorVersion > 6) or
                ((Win32MajorVersion = 6) and (Win32MinorVersion >= 2));
    end;

  begin
    Result := SERVICE_ACCEPT_SHUTDOWN;
    if doAllowStop in FDAemon.Definition.Options then
      Result := Result or SERVICE_ACCEPT_STOP;
    if doAllowPause in FDAemon.Definition.Options then
      Result := Result or SERVICE_ACCEPT_PAUSE_CONTINUE;
    if wccNetBindChange in ACodes then
      Result := Result or SERVICE_ACCEPT_NETBINDCHANGE;
    if wccParamChange in ACodes then
      Result := Result or SERVICE_ACCEPT_PARAMCHANGE;
    if wccPreShutdown in ACodes then
      Result := Result or SERVICE_ACCEPT_PRESHUTDOWN;
    if wccShutdown in ACodes then
      Result := Result or SERVICE_ACCEPT_SHUTDOWN;
    if wccHardwareProfileChange in ACodes then
      Result := Result or SERVICE_ACCEPT_HARDWAREPROFILECHANGE;
    if wccPowerEvent in ACodes then
      Result := Result or SERVICE_ACCEPT_POWEREVENT;
    if wccSessionChange in ACodes then
      Result := Result or SERVICE_ACCEPT_SESSIONCHANGE;
    if (wccTimeChange in ACodes) and IsWindows7OrNewer then
      Result := Result or SERVICE_ACCEPT_TIMECHANGE;
    if (wccTriggerEvent in ACodes) and IsWindows8OrNewer then
      Result := Result or SERVICE_ACCEPT_TRIGGEREVENT;
    if (wccUserModeReboot in ACodes) and IsWindows8OrNewer then
      Result := Result or SERVICE_ACCEPT_USERMODEREBOOT;
  end;

Var
  S : String;
  DD : TDaemonDef;
  SS : TServiceStatus;
  WB : TWinBindings;

Const
  WinServiceStatus : array[TCurrentStatus] of Integer
                   = (SERVICE_STOPPED, SERVICE_START_PENDING,
                      SERVICE_STOP_PENDING, SERVICE_RUNNING,
                      SERVICE_CONTINUE_PENDING, SERVICE_PAUSE_PENDING,
                      SERVICE_PAUSED);
  PendingStatus : set of TCurrentStatus
                = [csStartPending, csStopPending,
                   csContinuePending,csPausePending];

begin
  If not Assigned(FDaemon) then
    begin
    Application.Log(etError,Format(SErrNoDaemonForStatus,[Name]));
    Exit;
    end;
  DD:=FDaemon.Definition;
  If not Assigned(DD) then
    begin
    Application.Log(etError,Format(SErrNoDaemonDefForStatus,[Name]));
    Exit;
    end;
  If DD.LogStatusReport then
    With FDaemon do
      begin
      S:=Format(SDaemonStatus,[Definition.DisplayName, CurrentStatusNames[Status]]);
      Application.Log(etInfo,S);
      {$ifdef svcdebug}DebugLog(S);{$endif svcdebug}
      end;
  FillChar(SS,SizeOf(SS),0);
  WB:=DD.WinBindings;
  with SS do
    begin
    dwWaitHint := WB.WaitHint;
    dwServiceType :=WinServiceTypes[WB.ServiceType];
    if (FDaemon.Status=csStartPending) then
      dwControlsAccepted := 0
    else
      dwControlsAccepted := GetAcceptedCodes(WB.AcceptedCodes);
    if (FDaemon.Status in PendingStatus) and (FDaemon.Status = LastStatus) then
      Inc(FCheckPoint)
    else
      FCheckPoint := 0;
    dwCheckPoint:=FCheckPoint;
    FLastStatus := FDaemon.Status;
    dwCurrentState := WinServiceStatus[FDaemon.Status];
    dwServiceSpecificExitCode:=WB.ErrCode;
    if (WB.ErrCode<>0) then
      dwWin32ExitCode:=ERROR_SERVICE_SPECIFIC_ERROR
    else
      dwWin32ExitCode := WB.Win32ErrCode;
    if not SetServiceStatus(TSCMData(FSysData).FHandle, SS) then
      Application.Log(etError,SysErrorMessage(GetLastError));
  end;
end;

{ ---------------------------------------------------------------------
  Global initialization/Finalization
  ---------------------------------------------------------------------}

Procedure SysInitDaemonApp;


begin
end;


Procedure SysDoneDaemonApp;

begin
end;


